package com.atom.module.lib.opengles.shader

import android.graphics.RectF
import android.opengl.GLES30
import com.atom.module.lib.opengles.code.*
import com.atom.module.lib.opengles.draw.Gl2dDrawable
import com.atom.module.lib.opengles.draw.GlDrawable
import com.atom.module.lib.opengles.texture.GlTexture
import com.atom.module.lib.opengles.util.BufferUtils
import com.atom.module.lib.opengles.util.MatrixUtils
import com.atom.module.lib.opengles.util.ShaderUtils
import kotlin.jvm.JvmOverloads

@Suppress("unused")
open class GlTextureProgram(
    vertexCode: String = TextureVShader.create(),
    fragmentCode: String = TextureFShader.create(),
    val glTexture: GlTexture = GlTexture()
) :
    GlProgram(vertexCode, fragmentCode) {

    var textureTransform: FloatArray = MatrixUtils.IDENTITY_MATRIX.clone()
    private var textureCoordsBuffer = BufferUtils.floatBuffer(8)

    private val lastDrawableBounds = RectF()
    private var lastDrawableVersion = -1
    private var lastDrawable: Gl2dDrawable? = null

    override fun onPreDraw(drawable: GlDrawable, modelViewProjectionMatrix: FloatArray) {
        super.onPreDraw(drawable, modelViewProjectionMatrix)
        if (drawable !is Gl2dDrawable) {
            throw RuntimeException("GlTextureProgram only supports 2D drawables.")
        }

        glTexture.bind()

        // Pass the MVP matrix.
        TextureVShader.uniform_mvp_matrix_name.glUniformMatrix4fv(
            handle, 1,
            false, modelViewProjectionMatrix, 0
        )
        ShaderUtils.checkGLError("glUniformMatrix4fv")

        // Pass the texture transformation matrix.
        TextureVShader.uniform_texture_matrix_name.glUniformMatrix4fv(
            handle, 1,
            false, textureTransform, 0
        )
        ShaderUtils.checkGLError("glUniformMatrix4fv")

        // Pass the vertices position.
        TextureVShader.attribute_position_name.also {
            it.glEnableVertexAttribArray(handle)
            ShaderUtils.checkGLError("glEnableVertexAttribArray")
            it.glVertexAttribPointer(
                handle, 2,
                GLES30.GL_FLOAT,
                false,
                drawable.vertexStride,
                drawable.vertexArray
            )
            ShaderUtils.checkGLError("glVertexAttribPointer")
        }

        // We must compute the texture coordinates given the drawable vertex array.
        // To do this, we ask the drawable for its boundaries, then apply the texture
        // onto this rect.
        // Compute only if drawable changed. If the version has not changed, the
        // textureCoordsBuffer should be in a good state already - just need to rewind.
        if (drawable != lastDrawable || drawable.vertexArrayVersion != lastDrawableVersion) {
            lastDrawable = drawable
            lastDrawableVersion = drawable.vertexArrayVersion
            drawable.getBounds(lastDrawableBounds)
            val coordinates = drawable.vertexCount * 2
            if (textureCoordsBuffer.capacity() < coordinates) {
                textureCoordsBuffer = BufferUtils.floatBuffer(coordinates)
            }
            textureCoordsBuffer.clear()
            textureCoordsBuffer.limit(coordinates)
            for (i in 0 until coordinates) {
                val isX = i % 2 == 0
                val value = drawable.vertexArray.get(i)
                val min = if (isX) lastDrawableBounds.left else lastDrawableBounds.bottom
                val max = if (isX) lastDrawableBounds.right else lastDrawableBounds.top
                val texValue = computeTextureCoordinate(i / 2, drawable, value, min, max, isX)
                textureCoordsBuffer.put(texValue)
            }
        }
        textureCoordsBuffer.rewind()

        TextureVShader.attribute_texture_coord_name.also {
            it.glEnableVertexAttribArray(handle)
            ShaderUtils.checkGLError("glEnableVertexAttribArray")
            it.glVertexAttribPointer(
                handle, 2,
                GLES30.GL_FLOAT,
                false,
                drawable.vertexStride,
                textureCoordsBuffer
            )
            ShaderUtils.checkGLError("glVertexAttribPointer")
        }
    }

    // Returns the texture value for a given drawable vertex coordinate,
    // considering that texture values go from 0 to 1.
    protected open fun computeTextureCoordinate(
        vertex: Int,
        drawable: Gl2dDrawable,
        value: Float,
        min: Float,
        max: Float,
        horizontal: Boolean
    ): Float {
        val fraction = (value - min) / (max - min)
        return 0F + fraction * 1F // in tex coords
    }

    override fun onPostDraw(drawable: GlDrawable) {
        super.onPostDraw(drawable)
        TextureVShader.attribute_position_name.glDisableVertexAttribArray(handle)
        TextureVShader.attribute_texture_coord_name.glDisableVertexAttribArray(handle)
        glTexture.unbind()
        ShaderUtils.checkGLError("onPostDraw end")
    }

    override fun release() {
        super.release()
        glTexture.release()
    }
}

object TextureVShader : GlCode() {

    val uniform_mvp_matrix_name: GLUniformVariable =
        GLUniformVariable(GLClass.Mat4.create("uMVPMatrix"))
    val uniform_texture_matrix_name: GLUniformVariable =
        GLUniformVariable(GLClass.Mat4.create("uTexMatrix"))

    val attribute_position_name: GLAttributeVariable =
        GLAttributeVariable(GLClass.Vec4.create("aPosition"))
    val attribute_texture_coord_name: GLAttributeVariable =
        GLAttributeVariable(GLClass.Vec4.create("aTextureCoord"))

    val varying_textureCoord_name: GLVaryingVariable =
        GLVaryingVariable(GLClass.Vec2.create("vTextureCoord"))

    override fun create(): String {
        return """
                    $uniform_mvp_matrix_name;
                    $uniform_texture_matrix_name;
                    $attribute_position_name;
                    $attribute_texture_coord_name;
                    $varying_textureCoord_name;
                    void main() {
                        gl_Position = ${uniform_mvp_matrix_name.name()} * ${attribute_position_name.name()};
                        ${varying_textureCoord_name.name()} = (${uniform_texture_matrix_name.name()} * ${attribute_texture_coord_name.name()}).xy;
                    }
                    """.trimIndent()
    }
}

object TextureFShader : GlCode() {

    val uniform_texture_name: GLUniformVariable =
        GLUniformVariable(GLClass.SamplerExternalOES.create("sTexture"))

    override fun create(): String {
        return """
                #extension GL_OES_EGL_image_external : require
                precision mediump float;
                
                ${uniform_texture_name};
                
                ${TextureVShader.varying_textureCoord_name};
                
                void main() {
                    gl_FragColor = texture2D(${uniform_texture_name.name()}, ${TextureVShader.varying_textureCoord_name.name()});
                }
                """.trimIndent()
    }
}

object TextureFShaderDefault : GlCode() {

    val uniform_texture_name: GLUniformVariable =
        GLUniformVariable(GLClass.Sampler2D.create("sTexture"))

    override fun create(): String {
        return """
                #extension GL_OES_EGL_image_external : require
                precision mediump float;
                
                ${uniform_texture_name};
                ${TextureVShader.varying_textureCoord_name};
                
                void main() {
                    gl_FragColor = texture2D(${uniform_texture_name.name()}, ${TextureVShader.varying_textureCoord_name.name()});
                }
                """.trimIndent()
    }
}